Skip to content

refactor: sensors abstraction - #4

Open
wispl wants to merge 1 commit into
mainfrom
sensors-abstraction
Open

refactor: sensors abstraction#4
wispl wants to merge 1 commit into
mainfrom
sensors-abstraction

Conversation

@wispl

@wispl wispl commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

@wispl
wispl force-pushed the sensors-abstraction branch 2 times, most recently from bb87256 to 5f87fa0 Compare August 2, 2026 21:39
@wispl
wispl force-pushed the sensors-abstraction branch from 5f87fa0 to d437135 Compare August 2, 2026 21:52
@wispl

wispl commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Don't merge yet, just putting it here for early review

@dmanslick

Copy link
Copy Markdown

Looks good, assuming you just need to test this? Also, I feel like the "handle_as" functions would be better named "handle_get_as"

@ncorrea210

Copy link
Copy Markdown
Contributor

I guess I am just not 100% convinced yet that we need this level of abstraction for our sensors? Do we get a lot out of it in terms of maintainability and ease of understanding code? I wonder if it would be easier to just make specific implementations for each sensor, and let it be up to the device to determine how they want to use all that.

Comment thread include/defs.h
} uart;
struct handle_uart { UART_HandleTypeDef *handle; };

#define HANDLE_QSPI(handle) \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HANDLE_UART?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, should be handle QSPI

Comment thread include/defs.h
Comment on lines +52 to +66
#define HANDLE_SPI(in_handle, in_port, in_pin) \
(struct handle) { \
.protocol = SPI, \
.serial = { \
.spi = { \
.pin = (in_pin), \
.port = (in_port), \
.handle = (in_handle), \
} \
} \
};
#else
struct handle_spi { int placeholder; };
#define HANDLE_SPI(handle, port, pin) assert(0 & "SPI is not enabled!");
#endif

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I really love this HANDLE_SPI macro stuff. I think I see where you're coming from with having the stub implementation that would fail if someone in the device if struct handle_spi x = HANDLE_SPI(y,z,a), but unless a spi is explicitly configured from the .ioc file, there wouldn't be a well defined handle, or cs pin to add, to setup anyways? I think we are covering a case that is essentially impossible to run into.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still back and forth with the macro, you are right that compilation will not be allowed if we have an illdefined handle, but at the same time there is no error message shown indicating why. With the macro you get a clear message on why. I am willing to drop this if we think that is not an issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really think it will be an issue because you would not be able to put the periphal as the handle like you would be expected to. If someone tried to do struct handle_spi bmi088 = { .... , .handle = &hspi1} there would be several failures, one of them being that there isn't even an spi1 to be a handle in the first place.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is true. But the reason why you know that the compilation failed is due to missing modules is because you have worked on this before. For a new member they might assume that all modules are pulled in, or they might question why there isn't a SPI handle definition. That is not to say I am opposed to removing the macros.

Comment thread include/defs.h
Comment on lines +128 to +133
//// To create a handle, you MUST use the following methods
//// struct handle spi = HANDLE_SPI(&hspi1, port, pin);
//// struct handle i2c = HANDLE_I2C(&hi2c1, address);
//// struct handle uart = HANDLE_UART(&huart1);
//// struct handle qspi = HANDLE_QSPI(&hqspi);
//// Then you may pass the handle to the initialization function of a device.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not certain that this abstraction is needed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment.

Comment thread include/defs.h
Comment on lines +166 to +181
//// | Serial API Abstractions |
//// All serial protocols implement an API with the same write and read
//// functions, with differing implmentations of course. The serial api contains
//// the handle and is requested by a device in its initialization function.
////
//// See the currently provided drivers for an example. But the gist is
//// int8_t sensor_init(struct sensor_ctx *ctx, struct sensor *sensor, struct handle *handle)
//// {
//// assert(handle->protocol == SPI);
//// sensor->ctx = ctx;
//// sensor->read = sensor_read;
//// serial_api_spi(ctx->api, handle);
//// }
//// Of course you may decide to support multiple protocols if you wish, which
//// this abstraction makes really simple. You would pass `api` around and call
//// `api->write(...)` and `api->read(...)` for writing and reading respectively.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the comments, they are very helpful (nothing to resolve I just want to say that I like them)

@ncorrea210

Copy link
Copy Markdown
Contributor

Just to be clear, I am by no means the architect of this code and you should talk to Dhruv about anything major with regard to that

@wispl

wispl commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

The main pro of abstracting them to this thin api class is that we keep all the #ifdefs in one place. The sensors don't (and shouldn't) care about what protocol is used. With previous approaches one of the big issues was how to handle

  1. compilation error due to missing protocol (no SPI, or no UART)
  2. choosing between protocols (this is especially more difficult with flash)

This abstraction, to some degree, resolves 1. by collecting all #ifdefs and protocol definitions in one file, and 2. by allowing to simply use the api->read and api->write, which are defined by the protocol_api which you can just switch with another such struct to change which protocol is used.

On the point of maintainability and readability, we concretely defined three actions that every sensor/device has to do

  1. writes: for flash this is writing data or for a sensor this could be writing a register value
  2. reads: for both flash and sensor, this is reading values, either from a register or from the NAND
  3. command: this is usually for flash, which is to send a single command without expecting any further writes or reads

This means every implementation after can simply think of writes, reads, and commands rather than the actual HAL commands. Bosch has already done something similar with their own read and write callbacks, albeit it was mandatory on their part.

On maintainability, since this is the way every device/sensor communicates, we can have the implementation in a single place, with much more eyes on it, rather than the same implementation scattered across the codebase. Sorry for the long reply and rambling.

@ncorrea210

ncorrea210 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

No need to apologize! I agree with the sentiment of wanting things in a core place. I have an idea to consider, I know you all seem to hate cpp but please, just hear me out...

Create an abstract interface class called something like PERIPHERAL, and in this class we get the functions like read, write, whatever else is needed. We then extend this class to SPI, QUAD_SPI, UART, etc. Note about optimization if that is a concern, you might think that vtables would slow things down but if the derived classes are marked final then vtables are optimized away. Then we have a sensor interface class that has the read function, and whatever else we really want but it seems like read is the only general function we need. Then we can extend that to each sensor type like bmi088 and the other ones. The constructor for those can take a PERIPHERAL, and we use that for reading and such.

Just a note, cpp is very standard in embedded. There is really nothing to lose by using cpp, we just need to be careful about nothing going too far, but I feel like this is a happy medium use of it. If you would all like to put my head on a stick for this though that's fine, I just want to bring this idea up. Additionally, we get things like static_assert and also can be way more particular about what type of casting we are doing. Also the constexpr keyword is just excellent.

If this is something you guys would consider, I would be okay taking the lead in looking into it (unless you guys want to). I am just a fan of cpp and I think it could clean up a lot of our code. Also note that the HAL is the same for cpp and c, so that doesn't change.

To clear up what the workflow for this might be like, I am thinking that it might be like the following:
In the actual device, you would have SPI gyro_spi{&hspi1, gyro_cs_port, gyro_cs_pin) then BMI_088 bmi{gyro_spi, acc_spi}, then you would have an array with your sensors so std::array<SENSOR, NUM_SENSORS> sensors{bmi, bmp, mag}. Then in the task we can just walk through each sensor and do the reading and stuff. SENSOR will also be a very thin abstract template class (for CRTP) that would just have the read function that would just be

packet read()
{
  return static_cast<Derived*>(this)->do_read();
} 

To add on one more thing, I just want to note how this meets the goals you listed above. The different peripheral classes like SPI and QUAD_SPI can be made such that they only exist if those peripherals are enabled. If someone tries to setup a SPI instance when there is no spi enabled, they will find it difficult because SPI won't be defined. When it comes to choosing between protocols, each sensor can just take in PERIPHERAL and use its functions, so the sensor won't really care what is being passed in. To be sure there is no funny business like a UART being passed into a bmi though, we can add a peripheral type function to the PERIPHERAL class that returns the type of peripheral so we can check if it is supported. For the maintainability and readability side, writing sensors should be easier since they just take a peripheral and use the generic writes, reads, and commands in the abstract class and the correct functions will be called since we actually pass in the derived peripheral class.

@wispl

wispl commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

So I am not opposed to C++, but that is something you have to talk to Dhruv about. Your proposed architecture is already implemented in this PR but in C (I think that is what you intended?). Our analog of your PERIPHERAL is serial_api and the constructors of each sensors do take in an instance serial_api struct which they use for reads, and writes through api->write. And the sensor reading is currently how you described via

enum sensors {
  BMP581,
  BMI088,
  NUMBER_SENSORS
};
struct sensor sensors[NUMBER_SENSORS];

for (int i = 0; i < NUMBER_SENSORS; ++i) {
        if (sensors_res[i]) {
          sensors[i].read(sensors[i].ctx, &packet);
        }
}

Of course C++ will make this more clear and readable with classes, but again, this is something you should discuss with Dhruv first. I will say that moving to C++ will give us a more standardized unit testing framework via gTest.

@ncorrea210

Copy link
Copy Markdown
Contributor

Yes that was more or less the idea, to replicate what you have but in cpp. I suppose this is more of a personal preference, but I do think the cpp version of doing this is more clear and readable, while maintaining speed. Lets see if dhruv would be available to talk sometime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants